<?php
/*
    File: BetterValidation.php
    A better validation library

	Author: Richard Standbrook <richard@koorb.co.uk>
	Website: http://www.koorb.co.uk
*/

/*
	Class: ValidationException
	Exception class specificaly for BetterValidation exceptions
*/
class ValidationException extends Exception {
    
}

/*
	Class: BetterValidation
	Namespace class for custom functions
*/
class BetterValidation {
    
	static function value($arg)
	{
	    return $arg;
	}

	static function required($arg)
	{
	    return empty($arg);
	}
}


/*
    Class: ValidationRule
    Object defines the rule and checks the result of the function matches the
    value of <ValidationRule.valid_result>
    
    Arguments:
        function        - The function to pass the value to
        valid_result    - The expected result of the function to make it valid
*/
class ValidationRule {
    
    protected $function;
    protected $valid_result;
    
    /*
        Function: __construct
        
        Arguments:
            function        - The callback function
            valid_result    - The expected result the rule should return
    */
    public function __construct($function, $valid_result)
	{
        $this->function = $function;
        $this->valid_result = $valid_result;
    }
    
    /*
        Function: validate
        Run the rule
        
        Arguments:
            value   - The value to validate
    */
    public function validate($value)
	{
        if (is_callable($this->function)) {
            $result = call_user_func($this->function, $value);
            return ($result == $this->valid_result);
        } else {
            throw new ValidationException("Rule '$this->function' is not a callable function");
        }
    }

	/*
		Function: getValidResult
		Getter function for valid_result
	*/
	public function getValidResult()
	{
		return $this->valid_result;
	}
}


/*
    Class: ValidationRuleComparison
    Extends <ValidationRule> to add functionality to compare the field to the
    <ValidationRuleComparison.valid_result> rather that match it
    
    Arguments:
        function        - The function to pass the value to
        comparison      - The comparison operator
        valid_result    - The expected result of the function to make it valid
    
    Standard PHP comparison operators:
        ==      - Equal
        !=      - Not equal
        <       - Less than
        >       - Greater than
        <=      - Less than or equal to
        >=      - Greater than or equal to
*/
class ValidationRuleComparison extends ValidationRule {
    
    public $comparison;
    
    function __construct($function, $comparison, $valid_result)
	{
        parent::__construct($function, $valid_result);
        $this->comparison = $comparison;
    }
    
    /*
        Function: validate
        Run the rule
        
        Arguments:
            value           - The value to validate
            valid_result    - (optional) override the valid_result setup with 
                              the rule. Used when doing a `matches` rule to pass
                              in the value of the field to match
    */
    public function validate($value, $valid_result=null)
	{
        if($valid_result !== null) {
            $this->valid_result = $valid_result;
        }
        
        $result = call_user_func($this->function, $value);
        switch($this->comparison) {
            case '==':
                return ($result == $this->valid_result);
                break;

            case '!=':
                return ($result != $this->valid_result);
                break;

            case '<':
                return ($result < $this->valid_result);
                break;

            case '>':
                return ($result > $this->valid_result);
                break;

            case '<=':
                return ($result <= $this->valid_result);
                break;

            case '>=':
                return ($result >= $this->valid_result);
                break;
        }
    }
}

/*
    Class: ValidationRuleRegex
    Extends <ValidationRule> to add match the field to a regular expression
    
    Arguments:
        regex   - A regular expression to match the field against
*/
class ValidationRuleRegex extends ValidationRule {
    
    private $regex;
    
    /*
        Function: __construct
        
        Arguments:
            regex   - The regular expression string to validate against
    */
    public function __construct($regex)
	{
        $this->regex = $regex;
    }
    
    /*
        Function: validate
        Run the rule
        
        Arguments:
            value   - The value to validate
    */
    public function validate($value) 
	{
        return preg_match($this->regex, $value);
    }
}


/*
    Class: ValidationRules
*/
class ValidationRules {
    
    /*
        Properties:
            error_stacks        - An mdArray of error messages produced by the 
                                  rules
            rule_stacsk         - An mdArray of rules to validate
            patterns            - Regex patterns to find rules in the definition
            rule_patterns       - Regex patterns to validate special rules
            fail_on_first_error	- If true an exception will be thrown on the 
                                  first error and no further rules will be 
                                  processed
    */
    private $error_stacks = array();
    private $rule_stacks = array();
    private $patterns = array('regex_rule'	 => '/\|?(regex\[(\/.*?\/)\])\|?/',
        					  'default_rule' => '/\|?([a-z_]+)\[?([a-zA-Z0-9_-]+)?\]?/');
    private $rule_patterns = array('valid_email' => '/^[a-z0-9]+([_\\.-][a-z0-9]+)*@([a-z0-9]+([\.-][a-z0-9]+)*)+\\.[a-z]{2,}$/i');
    private $fail_on_first_error = false;
    
	/*
		Function: __construct
		Quick start constructor, allows you to pass an array of rules rather 
		than calling addRules repeatedly
		
		Arguments:
			rules				- Array of rules to validate
            fail_on_first_error	- (optional) If true an exception will be thrown
								  on the first error and no further rules will
                                  be processed. Defaults to false
	*/
    public function __construct($rules, $fail_on_first_error=false)
	{
        $this->fail_on_first_error = $fail_on_first_error;
        
        foreach ($rules as $name => $definition) {
            $this->addRules($name, $definition);
        }
    }
    
    /*
        Function: parseDefinition
        Parses the rules definition to determin what rules it contains
        
        Arguments:
            definition  - This is a pipe seperated list of rules to validate 
                          the field against
        
        Example:
            Multiple rules
            <code>
            required|min_length[5]|max_length[128]
            </code>
            
            Or a single rule
            <code>
            regex[/[a-zA-Z0-9_]/]
            </code>
    */
    private function parseDefinition($definition)
	{
        $rules = array();
        $rule_stack = array();
        
        // check for a regex rule first and remove it from the definition, 
        // it may contain unusual characters and we need it out the way
        $c = preg_match_all($this->patterns['regex_rule'], $definition, $regexrules);

        if ($c == 1) {
            $rules['regex'] = $regexrules[2][0];
            $definition = str_replace($regexrules[0][0], '', $definition);
        } elseif ($c > 1) {
            throw new ValidationException('Too many regex rules');
        }
        
        // get all the other rules
        foreach(explode('|', $definition) as $def) {
            preg_match($this->patterns['default_rule'], $def, $match);
            $rules[$match[1]] = isset($match[2]) ? $match[2] : true;
        }
        
        // add these rules to the rule_stack
        foreach($rules as $rule => $value) {
            
            switch($rule) {
                default:
                    throw new ValidationException("Unkown validation rule '$rule'");
                    break;
                    
                case 'required':
                    $rule_stack[$rule] = new ValidationRule('BetterValidation::required', false);
                    break;

                case 'min_length':
                    $rule_stack[$rule] = new ValidationRuleComparison('strlen', '>=', $value);
                    break;

                case 'max_length':
                    $rule_stack[$rule] = new ValidationRuleComparison('strlen', '<=', $value);
                    break;

                case 'matches':
                    $rule_stack[$rule] = new ValidationRuleComparison('BetterValidation::value', '==', $value);
                    break;

                case 'regex': 
                    $rule_stack[$rule] = new ValidationRuleRegex($value);
                    break;

                case 'valid_email': 
                    $rule_stack[$rule] = new ValidationRuleRegex($this->rule_patterns[$rule]);
                    break;
            }
        }
        
        return $rule_stack;
    }
    
    /*
        Function: raiseError
        Adds the rule that failed to the appropriate error stack
        
        Arguments:
            name    - Name of the error stack
            type    - The rule type that faild validation
    */
    public function raiseError($name, $type)
	{
        $this->error_stacks[$name][] = $type;
    }
    
    /*
        Function: getErrors
        Return details of the rules that failed
        
        Arguments:
            name    - (optional) Name of the field to get the errors for, 
                      otherwise all error stacks are returned
    */
    public function getErrors($name=null) 
	{
        if ($name)
			return $this->error_stacks[$name];
		
        return $this->error_stacks;
    }
    
    /*
        Function: countErrors
        Return the number of errors that occurred
    */
    public function countErrors()
	{
        return count($this->error_stacks, COUNT_RECURSIVE)-count($this->error_stacks);
    }
    
    /*
        Function: addRules
        Parses the rules definition and adds it to rule stack
        
        Arguments:
            name        - Name of the rule stack
            definition  - The rule definition string
    */
    public function addRules($name, $definition)
	{
        $this->rule_stacks[$name] = $this->parseDefinition($definition);
		return $this;
    }
    
    /*
        Function: validate
        
        Arguments:
            data    			- Array of data to validate
			fail_on_first_error	- (optional) Override global setting to fail the 
								  first time a rule fails validation
    */
    public function validate($data, $fail_on_first_error=null)
	{
        $this->data = $data;
        
        if ($fail_on_first_error !== null) {
            $this->fail_on_first_error = $fail_on_first_error;
        }
        
        foreach ($this->rule_stacks as $name => $rule_stack) {

            foreach ($rule_stack as $type => $rule) {
                // if this is a 'matches' rule we will need pass the value of the field to match
                $valid_result = ($type == 'matches') ? $data[$rule->getValidResult()] : null;
                
                if ($rule->validate($data[$name], $valid_result)) {
                    continue;
                } else {
                    if ($this->fail_on_first_error) {
                        throw new ValidationException("$name failed on $type");
                    } else {
                        $this->raiseError($name, $type);
                    }
                }
            }
        }
        
        return empty($this->error_stacks);
    }
}


/*
    Class: Validation
    Codeigniter library class
*/
class Validation {
    
    private $data;
    private $rules;
    
    public function run($data=null, $rules=null)
	{
        if ($rules) {
			$this->setRules($rules);
		}
		
        if ($data) {
			$this->setData($data);
		}
		
        $this->rules->validate($this->data);
    }
    
    public function setData($data)
	{
        $this->data = $data;
    }
    
    public function setRules($rules)
	{
        $this->rules = $rules;
    }
}